﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading.Tasks;

namespace UndoRedo.UnitTests
{
    [TestClass]
    public class SingleTargetUndoRedoTests
    {
        [TestMethod]
        public void AllowsAccessOnlyOnCreatingThreadForPushChange()
        { this.VerifyThreadAccessExceptionIsThrown(undoStack => { undoStack.PushChange(new ActionChangeInformation<object>(obj => { }, obj => { })); }); }

        [TestMethod]
        public void AllowsAccessOnlyOnCreatingThreadForUndo()
        { this.VerifyThreadAccessExceptionIsThrown(undoStack => { undoStack.Undo(); }); }

        [TestMethod]
        public void AllowsAccessOnlyOnCreatingThreadForRedo()
        { this.VerifyThreadAccessExceptionIsThrown(undoStack => { undoStack.Redo(); }); }

        [TestMethod, ExpectedException(typeof(InvalidOperationException))]
        public void DoesNotAllowNewChangeDuringUndo()
        {
            var undoStack = new SingleTargetUndoRedo<object>(new object());

            var changeToPushDuringUndo = new ActionChangeInformation<object>(obj => { }, obj => { });

            Action<object> newChangeAction = obj => { undoStack.PushChange(changeToPushDuringUndo); };
            undoStack.PushChange(new ActionChangeInformation<object>(newChangeAction, obj => { }));

            undoStack.Undo();
        }

        [TestMethod, ExpectedException(typeof(InvalidOperationException))]
        public void DoesNotAllowNewChangeDuringRedo()
        {
            var undoStack = new SingleTargetUndoRedo<object>(new object());

            var changeToPushDuringUndo = new ActionChangeInformation<object>(obj => { }, obj => { });

            Action<object> newChangeAction = obj => { undoStack.PushChange(changeToPushDuringUndo); };
            undoStack.PushChange(new ActionChangeInformation<object>(obj => { }, newChangeAction));

            //Need to make sure to run an undo first otherwise a redo won't run any changes (since there are no changes to run unless at least one action has been undone).
            undoStack.Undo();
            undoStack.Redo();
        }

        private void VerifyThreadAccessExceptionIsThrown(Action<SingleTargetUndoRedo<object>> method)
        {
            var undoStack = new SingleTargetUndoRedo<object>(new object());

            //Catch the raw exception on the task and return it rather than retrieving an aggregate exception on the originating thread and then inspecting it. Feels more proper to check it this way.
            Task<Exception> pushChangeTask = new Task<Exception>(() =>
            {
                Exception ex = null;
                try
                {
                    method(undoStack);
                }
                catch (Exception thrown)
                { ex = thrown; }

                return ex;
            });

            pushChangeTask.Start();

            Exception exceptionFromThread = (Exception)pushChangeTask.Result;
            Assert.AreEqual(typeof(ApplicationException), exceptionFromThread == null ? null : exceptionFromThread.GetType());
        }
    }
}
